import type { AObject } from "./AObject";
import type { RoutineInstance } from "./ARoutine";
import { AZone } from "./AZone";
import { Agent } from "./Agent";
import * as Debug from "./Debug";
import { NAlgorithms } from "./NarrativeAlgorithms";
import { HistoryEntry, N, Sentence, SentenceFlags, type SentenceContent, NUtils } from "./NarrativeBase";
import type { NarrativeContext } from "./NarrativeContext";
import { World, type TypedScheduledProcess } from "./WorldBase";

export const VERSION_ENGINE = 'alpha 0.2.1';

export enum Direction {
    E, S, W, N, NE, NW, SE, SW, Up, Down
}

export let DirectionNames = [
    '东', '南', '西', '北', '东北', '西北', '东南', '西南', '上', '下'
]

export enum Visibility {
    Visible, NotVisible, Unclear
}

export enum CustomRoutineType {
    Press, Push, Pull, Shake, Touch, Smell, Listen
}

/**
 * Remarks: `Toggle` can mean pressing (activating) for a button, pulling/pushing 
 * (toggling its state) for a lever, or similar actions.
 */
export enum ObjectHandlerType {
    Examine, Take, Drop, Open, Close, TurnOn, TurnOff, Enter, Read
}

export enum ZoneHandlerType {
    Describe, GoDirection, Escape, Smell, Listen, ArriveAt
}

export type CustomRoutineKeys = keyof typeof CustomRoutineType;
export type ObjectHandlerKeys = keyof typeof ObjectHandlerType;
export type ZoneHandlerKeys = keyof typeof ZoneHandlerType;

export enum ContainerType {
    NotContainer, 
    Surface,    // e.g. tables
    Interior    // e.g. bags, bottles, baskets
}

export enum ActionReportType {
    RunTowards,
    WalkTowards,
    TurnOnOff,
    OpenClose,
    RunIntoObject,
    WalkIntoObject,
    PickUpObject,
    HoldUpObject,
    DropObject,
    PutInContainer,
    TouchObject,
    ShakeObject,
    LockUnlockObject,
    WriteSomething,
    EraseSomething,
    HeardNothingSpecial,
    SmelledNothingSpecial,
    FeltNothingSpecial,
    LockabilityInfo,
    UnLockabilityInfo,
}

export type ActionReportArguments = {
    type: ActionReportType,
    object?: AObject,
    instrument?: AObject,
    container?: AObject,
    zone?: AZone,
    state?: boolean
}

/**
 * Checking if some action is not successful:
 * ```
 * if (actionReturn) return actionReturn;
 * // or:
 * if (result.type) return actionReturn;
 * ```
 */
export enum RoutineResultType {
    Ok = 0,
    Interrupted,
    Continue, ContinueSilent,
    CustomError, Bug,

    CantDoWhileHoldingItem,
    NoStrength,
    NotEnoughStrength,
    NoPassage,
    CantEscape,
    ObjectNotOpenable,
    ObjectLocked,
    ObjectAlreadyOpenedClosed,
    ObjectNotSwitch,
    ObjectAlreadyOnOff,
    ObjectNotEnterable,
    ObjectNotTakable,
    YouAlreadyHaveThis,
    YouDontHaveThis,
    ObjectAlrealyInContainer,
    ObjectNotContainer,
    CantPutObjectInItself,
    ObjectAlreadyLockedUnlocked,
    CantLockUnlockObject,
    ObjectNotWritable,
    ObjectNotWritingInstrument,
    ObjectNotEraser,
    CantDoWhileNotOpen,             // obj
    InventoryFull,                  // obj
    OpeningTooNarrow,               // cont obj
    ContainerTooFull,               // cont obj
    ObjectLargerThanMouthPut,       // cont obj
    ObjectLargerThanMouthTake,      // cont obj
    ObjectNoWriting,                // obj
    ObjectNotShakable,              // obj
    NoWritingSpace,                 // obj
    NoWritingToErase,               // obj
    KeyNeeded,
    IncorrectKey,
    NotUseful,
}

export type RoutineResult = {
    type: RoutineResultType,
    disruptive: boolean,
    object?: AObject,
    container?: AObject,
    state?: boolean
}

export let RoutineResult = {} as {
    -readonly [key in keyof typeof RoutineResultType]: 
        (params?: Partial<RoutineResult>, disruptive?: boolean) => RoutineResult};
for (const i in RoutineResultType) {
    let v = RoutineResultType[i];
    if (typeof v == 'string') {
        RoutineResult[v as any] = (params = {}, disruptive = false) => {
            let obj = {type: parseInt(i), disruptive: disruptive};
            Object.assign(obj, params);
            return obj;
        }
    }
}

export type ScheduledRoutine = TypedScheduledProcess<RoutineResult>;

export enum StatusType {
    Disturbing, // always displayed when it is disturbing
    Changed,    // always displayed when changed. // ADV_TODO: this is not gonna work
    Normal      // only displayed when first encountered; a mild long-term status
}

export class AHistory {
    // ADV_FIXME: I don't know how effective this is
    private _newLength = 0;
    private _lastTime = 0;
    list: HistoryEntry[] = []; // from old to new

    constructor(private _parent: AZone | Agent) {}

    pushEntry(entry: HistoryEntry) {
        Debug.assert(entry.startTime !== undefined);
        if (this.list.length > 0)
            Debug.assert(entry.startTime >= this.list[0].startTime!)
        Debug.unregister(entry);
        this.list.push(entry);

        if (this._parent instanceof Agent && entry.type == 'action')
            this._parent.parentZone.history.pushEntry(entry);
    }

    filter(context: NarrativeContext,  start?: number, 
        end: number = World.schedule.absoluteTime, exclude?: Set<HistoryEntry>)
            : ["meta" | "annotation" | "narrative", Sentence[]][]
    {
        // ADV_FIXME: I believe this is bad. One must at least forward the entries upwards when submitting them as there is no other way to keep them stored when the senders are away; consequently, if we don't implement some fancy visibility checking mechanisms, there must be issues relating to long durative actions not being visible when the sender traverses through multiple places. Also, there are problems with momentary actions occurring at 'fringy' time points. Last, the filtering out of duplicate entries is very awkward.
        let local = this.#filterLocal(context, start, end, exclude);
        let set = new Set([...this.list, ...(exclude ?? [])]);
        if (start === undefined) start = this._lastTime;
        let external = (this._parent instanceof AZone 
            ? this._parent.parent
            : this._parent.parentZone)?.history.filter(context, start, end, set);
        return [...local, ...external ?? []];
    }

    #filterLocal(context: NarrativeContext, start?: number, 
        end: number = World.schedule.absoluteTime, exclude?: Set<HistoryEntry>)
            : ["meta" | "annotation" | "narrative", Sentence[]][]
    {
        let from = start === undefined ? this.list.slice(0, this._newLength) : this.list;
        let entries: HistoryEntry[] = [];
        if (start === undefined) {
            entries = this.list.slice(this._newLength);
            start = this._lastTime;
        }
        entries.push(...from.filter((x) => 
            (x.startTime! >= start! || x.endTime === undefined || x.endTime > start!) 
                && x.startTime! <= end));
        if (exclude) entries = entries.filter((x) => !exclude.has(x));

        const collection: ['meta' | 'annotation' | 'narrative', Sentence[]][] = [];
        if (entries.length == 0) return collection;

        let groupKind: 'meta' | 'annotation' | 'narrative';
        let sentences: Sentence[] = [];
        for (let ent of entries) {
            Debug.assert(ent.startTime !== undefined);
            const isFromFreeSubject = 
                ent.sender && context.freeIndirectSubject.has(ent.sender);
            if (ent.type == 'thought' && !isFromFreeSubject) continue;
            if (ent.type == 'action' && ent.implicit && isFromFreeSubject) continue;


            const thisKind = (ent.type == 'action' || ent.type == 'thought') 
                            ? 'narrative' : ent.type;
            if (sentences.length == 0) {
                groupKind = thisKind;
                console.log(`<!${thisKind}>`);
            } else if (thisKind != groupKind!) {
                collection.push([groupKind!, sentences]);
                sentences = [];
                groupKind = thisKind;
                console.log(`<!${thisKind}>`);
            }
            
            let s: Sentence;
            // ADV_FIXME: don't know how correct this is itself, but this is for mitigating the fact that the printing of new History entries precedes the formal setting of interruption state in Agent routines.
            if (ent.interrupted || 
                (ent.endTime === undefined && ent.content instanceof Sentence))
            {
                console.log('encountering interrupted:', ent);
                if (ent.interruptedForm) {
                    s = ent.interruptedForm;
                    s.flags.add(SentenceFlags.Unfinished);
                } else {
                    let orig = ent.content instanceof Sentence
                             ? ent.content.elements
                             : ent.content.progressive?.elements 
                               ?? N.text('progressive:', ent.debugString);
                    let clause = NAlgorithms.generateWhenClause(orig);
                    if (clause) {
                        s = new Sentence([clause]);
                        s.flags.add(SentenceFlags.Unfinished);
                    } else
                        s = new Sentence(N.text('[interrupted:', ...orig, ']'));
                }
            } else if (ent.content instanceof Sentence) {
                s = ent.content as Sentence;
            } else {
                if (ent.startTime >= start) {
                    if (ent.endTime && ent.endTime <= end) s = ent.content.perfective 
                        ?? new Sentence(N.text('[perfective:', ent.debugString, ']'));
                    else s = ent.content.inchoative 
                        ?? new Sentence(N.text('[inchoative:', ent.debugString, ']'));
                } else {
                    if (ent.endTime && ent.endTime <= end) s = ent.content.cessative 
                        ?? new Sentence(N.text('[cessative:', ent.debugString, ']'));
                    else s = ent.content.progressive 
                        ?? new Sentence(N.text('[progressive:', ent.debugString, ']'));
                }
            }
            s.group = ent.group;
            console.log(s);
            sentences.push(s);
        }
        collection.push([groupKind!, sentences]);
        return collection;
    }

    markAsOld() { 
        this._newLength = this.list.length;
        this._lastTime = World.schedule.absoluteTime;
    }
}

export type DestinationData = [
    zone: AZone,
    distance: number // in meters
];

export type StatusData = [
    text: string,
    type: StatusType
];

// I would like to use (this: T, ag: Agent) since calling that is more concise, but that 
// will prevent the usage of arrow functions as Descriptor and ActionHandler
export type Descriptor<T> = 
    (self: T, ag: Agent) => SentenceContent;

export type NullableDescriptor<T> = 
    (self: T, ag: Agent) => SentenceContent | null;

export type ActionHandler<T> = 
    (self: T, ag: Agent, rout: RoutineInstance) => ScheduledRoutine;

export type LockHandler<T> = 
    (self: T, ag: Agent, key: AObject | null) => ScheduledRoutine;

export type StatusDescriptor<T> =
    (self: T, ag: Agent) => StatusData | null;